iT邦幫忙

2024 iThome 鐵人賽

DAY 16
0
Modern Web

使用Nextjs及Xata全端開發 打造屬於你的討論版SaaS系列 第 16

[Day16] 查看回報內容 - 使用authjs 驗證api

  • 分享至 

  • xImage
  •  

查看回報

為了方便我們快速引導至Dashboard畫面

我們可以在最上方的連結中 加入dashboard的連結

src/components/layout/Title.tsx

新增以下的連結

<Link
  href={`/dashboard/main`}
  className="text-gray-400 hover:text-gray-600 flex items-center"
>
  Dashboard <Link2 className="ml-1 h-3 w-3" />
</Link>

然後你就可以點擊Dashboard連結 看到Dashboard畫面了

新增回報列表

我們一樣先安裝今天會用到的套件

npx shadcn@latest add tooltip toast table

並且建立src/components/service/ReportList.tsx

"use client";
import React, { useCallback, useEffect, useState } from "react";
import { Trash2, ExternalLink, FileX, MessageSquareX } from "lucide-react";
import axios from "axios";
import {
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableHeader,
  TableRow,
} from "@/components/ui/table";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { ReportsRecord } from "@/xata";
import LoadingOverlay from "../commons/LoadingOverlay";
import { useToast } from "@/components/hooks/use-toast";
import {
  Tooltip,
  TooltipContent,
  TooltipProvider,
  TooltipTrigger,
} from "@/components/ui/tooltip";
import {
  AlertDialog,
  AlertDialogAction,
  AlertDialogCancel,
  AlertDialogContent,
  AlertDialogDescription,
  AlertDialogFooter,
  AlertDialogHeader,
  AlertDialogTitle,
  AlertDialogTrigger,
} from "@/components/ui/alert-dialog";

interface ReportListProps {
  serviceId: string;
}

const ReportList: React.FC<ReportListProps> = ({ serviceId }) => {
  const [reports, setReports] = useState<ReportsRecord[]>([]);
  const [isLoading, setIsLoading] = useState(true);
  const [selectedReports, setSelectedReports] = useState<string[]>([]);
  const [deletingItemId, setDeletingItemId] = useState<string | null>(null);
  const { toast } = useToast();

  const fetchReports = useCallback(async () => {
    if (!serviceId) return;
    setIsLoading(true);
    try {
      const response = await axios.get(`/api/service/${serviceId}/reports`);
      setReports(response.data);
    } catch (error) {
      console.error("Error fetching reports:", error);
    } finally {
      setIsLoading(false);
    }
  }, [serviceId]);

  useEffect(() => {
    fetchReports();
  }, [fetchReports]);

  const handleDeleteReports = async () => {
    setIsLoading(true);
    try {
      await axios.delete(`/api/service/${serviceId}/reports`, {
        data: { reportIds: selectedReports },
      });
      fetchReports();
      setSelectedReports([]);
      toast({
        title: "Success",
        description: "Report(s) has been deleted.",
        variant: "default",
      });
    } catch (error) {
      console.error("Error deleting reports:", error);
    }
  };

  const handleDeleteThreadOrReply = async (report: ReportsRecord) => {
    setIsLoading(true);
    setDeletingItemId(report.id);
    try {
      if (report.reply?.id || report.thread?.id) {
        await axios.delete(`/api/service/${serviceId}/reports`, {
          data: { reportIds: [report.id], deleteAssociated: true },
        });
        toast({
          title: "Success",
          description: "Thread or Reply has been deleted.",
          variant: "default",
        });
      } else {
        toast({
          title: "Info",
          description: "No associated thread or reply to delete.",
          variant: "default",
        });
      }
      fetchReports();
    } catch (error) {
      console.error("Error deleting thread or reply:", error);
      toast({
        title: "Error",
        description: "Failed to delete. Please try again.",
        variant: "destructive",
      });
    } finally {
      setDeletingItemId(null);
    }
  };

  const handleViewReport = (threadId: string, replyId?: string) => {
    const url = `/service/${serviceId}/${threadId}${
      replyId ? `#${replyId}` : `#${threadId}`
    }`;
    window.open(url, "_blank");
  };

  const handleSelectReport = (reportId: string) => {
    setSelectedReports((prev) =>
      prev.includes(reportId)
        ? prev.filter((id) => id !== reportId)
        : [...prev, reportId]
    );
  };

  const handleSelectAllReports = () => {
    setSelectedReports(
      selectedReports.length === reports.length
        ? []
        : reports.map((report) => report.id)
    );
  };

  return (
    <LoadingOverlay isLoading={isLoading}>
      <Card>
        <CardHeader>
          <CardTitle>Reports</CardTitle>
        </CardHeader>
        <CardContent>
          <div className="mb-4">
            <TooltipProvider>
              <Tooltip>
                <TooltipTrigger asChild>
                  <AlertDialog>
                    <AlertDialogTrigger asChild>
                      <Button
                        className="ml-1"
                        size="icon"
                        variant="destructive"
                        disabled={selectedReports.length === 0}
                      >
                        <Trash2 className="h-4 w-4" />
                      </Button>
                    </AlertDialogTrigger>
                    <AlertDialogContent>
                      <AlertDialogHeader>
                        <AlertDialogTitle>Delete Reports</AlertDialogTitle>
                        <AlertDialogDescription>
                          This action will delete the selected report records.
                          It will not affect the associated threads or replies.
                          Are you sure you want to proceed?
                        </AlertDialogDescription>
                      </AlertDialogHeader>
                      <AlertDialogFooter>
                        <AlertDialogCancel>Cancel</AlertDialogCancel>
                        <AlertDialogAction onClick={handleDeleteReports}>
                          Delete
                        </AlertDialogAction>
                      </AlertDialogFooter>
                    </AlertDialogContent>
                  </AlertDialog>
                </TooltipTrigger>
                <TooltipContent>
                  <p>
                    Delete selected report records (does not affect threads or
                    replies)
                  </p>
                </TooltipContent>
              </Tooltip>
            </TooltipProvider>
          </div>
          <Table>
            <TableHeader>
              <TableRow>
                <TableHead className="w-[50px]">
                  <Checkbox
                    checked={
                      selectedReports.length === reports.length &&
                      reports.length !== 0
                    }
                    onCheckedChange={handleSelectAllReports}
                  />
                </TableHead>
                <TableHead>Date</TableHead>
                <TableHead>Content</TableHead>
                <TableHead>Reporter IP</TableHead>
                <TableHead>Poster IP</TableHead>
                <TableHead>Actions</TableHead>
              </TableRow>
            </TableHeader>
            <TableBody>
              {reports.map((report) => (
                <TableRow key={report.id}>
                  <TableCell>
                    <Checkbox
                      checked={selectedReports.includes(report.id)}
                      onCheckedChange={() => handleSelectReport(report.id)}
                    />
                  </TableCell>
                  <TableCell>
                    {new Date(report.xata.createdAt).toLocaleString()}
                  </TableCell>
                  <TableCell>{report.content}</TableCell>
                  <TableCell>{report.userIp}</TableCell>
                  <TableCell>{report.reportedIp}</TableCell>
                  <TableCell>
                    <TooltipProvider>
                      <Tooltip>
                        <TooltipTrigger asChild>
                          <Button
                            disabled={!report.thread && !report.reply}
                            onClick={() =>
                              handleViewReport(
                                report.thread?.id || "",
                                report.reply?.id || ""
                              )
                            }
                            size="icon"
                            variant="outline"
                            className="mr-2"
                          >
                            <ExternalLink className="h-4 w-4" />
                          </Button>
                        </TooltipTrigger>
                        <TooltipContent>
                          <p>Open in new tab</p>
                        </TooltipContent>
                      </Tooltip>
                    </TooltipProvider>
                    <TooltipProvider>
                      <Tooltip>
                        <TooltipTrigger asChild>
                          <AlertDialog>
                            <AlertDialogTrigger asChild>
                              <Button
                                size="icon"
                                variant="destructive"
                                disabled={
                                  deletingItemId === report.id ||
                                  (!report.thread && !report.reply)
                                }
                              >
                                {report.reply?.id ? (
                                  <MessageSquareX className="h-4 w-4" />
                                ) : (
                                  <FileX className="h-4 w-4" />
                                )}
                              </Button>
                            </AlertDialogTrigger>
                            <AlertDialogContent>
                              <AlertDialogHeader>
                                <AlertDialogTitle>
                                  Delete {report.reply?.id ? "Reply" : "Thread"}
                                </AlertDialogTitle>
                                <AlertDialogDescription>
                                  This action will permanently delete the{" "}
                                  {report.reply?.id ? "reply" : "thread"}{" "}
                                  associated with this report. Are you sure you
                                  want to proceed?
                                </AlertDialogDescription>
                              </AlertDialogHeader>
                              <AlertDialogFooter>
                                <AlertDialogCancel>Cancel</AlertDialogCancel>
                                <AlertDialogAction
                                  onClick={() =>
                                    handleDeleteThreadOrReply(report)
                                  }
                                >
                                  Delete
                                </AlertDialogAction>
                              </AlertDialogFooter>
                            </AlertDialogContent>
                          </AlertDialog>
                        </TooltipTrigger>
                        <TooltipContent>
                          <p>
                            {report.reply?.id
                              ? "Delete Reply"
                              : "Delete Thread"}
                          </p>
                        </TooltipContent>
                      </Tooltip>
                    </TooltipProvider>
                  </TableCell>
                </TableRow>
              ))}
            </TableBody>
          </Table>
        </CardContent>
      </Card>
    </LoadingOverlay>
  );
};

export default ReportList;

然後你就可以在你的dashboard頁面中 看到ReportList了

發文者IP及回報者IP都是::1是因為我們都在本地使用 所以IP都是127.0.0.1

https://ithelp.ithome.com.tw/upload/images/20240905/20168796D4M91OTYRQ.png

實際在線上使用時 會是真實的IP

嘗試刪除report

我們這裡實作了兩種刪除

第一種是單純刪除回報 理由是可能誤報 或是單純判定不需要刪除
你可以點選左邊的checkbox 選擇多個report 然後按下Delete按鈕 就可以刪除多個report

https://ithelp.ithome.com.tw/upload/images/20240905/20168796HxyjVOOC3S.png

第二種是刪除report的thread或是reply
也就是判定需要刪除的討論串或回覆

直接點選action內的delete按鈕 你就可以將討論串或回覆刪除了

嘗試未登入時使用api

你可以直接在網址列輸入

http://localhost:3000/api/service/main/reports

應該可以看到你的回報

https://ithelp.ithome.com.tw/upload/images/20240905/20168796FtXn1Fxx0K.png

但是如果你開啟無痕視窗
然後再次開啟這個網址 應該會看到錯誤

https://ithelp.ithome.com.tw/upload/images/20240905/20168796zbZbGt2nSd.png

{"error":"You are not owner of service"}

這表示你的api是好好的在authjs的保護之下

總結

我們今天實作了查看report的功能

並且使用瀏覽器測試了看我們的api是否在authjs的保護之下


上一篇
[Day15] 回報機制
下一篇
[Day17] 來做服務的首頁吧 - Nextjs的ISR
系列文
使用Nextjs及Xata全端開發 打造屬於你的討論版SaaS30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言